// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
    using BadHttpRequestException = Microsoft.AspNetCore.Http.BadHttpRequestException;

    internal abstract partial class HttpProtocol : IHttpResponseControl
    {
        private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close");
        private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive");
        private static readonly byte[] _bytesTransferEncodingChunked = Encoding.ASCII.GetBytes("\r\nTransfer-Encoding: chunked");
        private static readonly byte[] _bytesServer = Encoding.ASCII.GetBytes("\r\nServer: " + Constants.ServerName);
        internal const string SchemeHttp = "http";
        internal const string SchemeHttps = "https";

        protected BodyControl? _bodyControl;
        private Stack<KeyValuePair<Func<object, Task>, object>>? _onStarting;
        private Stack<KeyValuePair<Func<object, Task>, object>>? _onCompleted;

        private readonly object _abortLock = new object();
        protected volatile bool _connectionAborted;
        private bool _preventRequestAbortedCancellation;
        private CancellationTokenSource? _abortedCts;
        private CancellationToken? _manuallySetRequestAbortToken;

        protected RequestProcessingStatus _requestProcessingStatus;

        // Keep-alive is default for HTTP/1.1 and HTTP/2; parsing and errors will change its value
        // volatile, see: https://msdn.microsoft.com/en-us/library/x13ttww7.aspx
        protected volatile bool _keepAlive = true;
        // _canWriteResponseBody is set in CreateResponseHeaders.
        // If we are writing with GetMemory/Advance before calling StartAsync, assume we can write and throw away contents if we can't.
        private bool _canWriteResponseBody = true;
        private bool _hasAdvanced;
        private bool _isLeasedMemoryInvalid = true;
        private bool _autoChunk;
        protected Exception? _applicationException;
        private BadHttpRequestException? _requestRejectedException;

        protected HttpVersion _httpVersion;
        // This should only be used by the application, not the server. This is settable on HttpRequest but we don't want that to affect
        // how Kestrel processes requests/responses.
        private string? _httpProtocol;

        private string? _requestId;
        private int _requestHeadersParsed;

        private long _responseBytesWritten;

        private HttpConnectionContext _context = default!;
        private RouteValueDictionary? _routeValues;
        private Endpoint? _endpoint;

        protected string? _methodText = null;
        private string? _scheme = null;
        private Stream? _requestStreamInternal;
        private Stream? _responseStreamInternal;

        public void Initialize(HttpConnectionContext context)
        {
            _context = context;

            ServerOptions = ServiceContext.ServerOptions;

            Reset();

            HttpResponseControl = this;
        }

        public IHttpResponseControl HttpResponseControl { get; set; } = default!;

        public ServiceContext ServiceContext => _context.ServiceContext;
        private IPEndPoint? LocalEndPoint => _context.LocalEndPoint;
        private IPEndPoint? RemoteEndPoint => _context.RemoteEndPoint;
        public ITimeoutControl TimeoutControl => _context.TimeoutControl;

        public IFeatureCollection ConnectionFeatures => _context.ConnectionFeatures;
        public IHttpOutputProducer Output { get; protected set; } = default!;

        protected IKestrelTrace Log => ServiceContext.Log;
        private DateHeaderValueManager DateHeaderValueManager => ServiceContext.DateHeaderValueManager;
        // Hold direct reference to ServerOptions since this is used very often in the request processing path
        protected KestrelServerOptions ServerOptions { get; set; } = default!;
        protected string ConnectionId => _context.ConnectionId;

        public string ConnectionIdFeature { get; set; } = default!;
        public bool HasStartedConsumingRequestBody { get; set; }
        public long? MaxRequestBodySize { get; set; }
        public MinDataRate? MinRequestBodyDataRate { get; set; }
        public bool AllowSynchronousIO { get; set; }

        /// <summary>
        /// The request id. <seealso cref="HttpContext.TraceIdentifier"/>
        /// </summary>
        public string TraceIdentifier
        {
            set => _requestId = value;
            get
            {
                // don't generate an ID until it is requested
                if (_requestId == null)
                {
                    _requestId = CreateRequestId();
                }
                return _requestId;
            }
        }

        public bool IsUpgradableRequest { get; private set; }
        public bool IsUpgraded { get; set; }
        public IPAddress? RemoteIpAddress { get; set; }
        public int RemotePort { get; set; }
        public IPAddress? LocalIpAddress { get; set; }
        public int LocalPort { get; set; }
        public string? Scheme { get; set; }
        public HttpMethod Method { get; set; }
        public string MethodText => ((IHttpRequestFeature)this).Method;
        public string? PathBase { get; set; }

        public string? Path { get; set; }
        public string? QueryString { get; set; }
        public string? RawTarget { get; set; }

        public string HttpVersion
        {
            get
            {
                if (_httpVersion == Http.HttpVersion.Http3)
                {
                    return AspNetCore.Http.HttpProtocol.Http3;
                }
                if (_httpVersion == Http.HttpVersion.Http2)
                {
                    return AspNetCore.Http.HttpProtocol.Http2;
                }
                if (_httpVersion == Http.HttpVersion.Http11)
                {
                    return AspNetCore.Http.HttpProtocol.Http11;
                }
                if (_httpVersion == Http.HttpVersion.Http10)
                {
                    return AspNetCore.Http.HttpProtocol.Http10;
                }

                return string.Empty;
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            set
            {
                // GetKnownVersion returns versions which ReferenceEquals interned string
                // As most common path, check for this only in fast-path and inline
                if (ReferenceEquals(value, AspNetCore.Http.HttpProtocol.Http3))
                {
                    _httpVersion = Http.HttpVersion.Http3;
                }
                else if (ReferenceEquals(value, AspNetCore.Http.HttpProtocol.Http2))
                {
                    _httpVersion = Http.HttpVersion.Http2;
                }
                else if (ReferenceEquals(value, AspNetCore.Http.HttpProtocol.Http11))
                {
                    _httpVersion = Http.HttpVersion.Http11;
                }
                else if (ReferenceEquals(value, AspNetCore.Http.HttpProtocol.Http10))
                {
                    _httpVersion = Http.HttpVersion.Http10;
                }
                else
                {
                    HttpVersionSetSlow(value);
                }
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void HttpVersionSetSlow(string value)
        {
            if (AspNetCore.Http.HttpProtocol.IsHttp3(value))
            {
                _httpVersion = Http.HttpVersion.Http3;
            }
            else if (AspNetCore.Http.HttpProtocol.IsHttp2(value))
            {
                _httpVersion = Http.HttpVersion.Http2;
            }
            else if (AspNetCore.Http.HttpProtocol.IsHttp11(value))
            {
                _httpVersion = Http.HttpVersion.Http11;
            }
            else if (AspNetCore.Http.HttpProtocol.IsHttp10(value))
            {
                _httpVersion = Http.HttpVersion.Http10;
            }
            else
            {
                _httpVersion = Http.HttpVersion.Unknown;
            }
        }

        public IHeaderDictionary RequestHeaders { get; set; } = default!;
        public IHeaderDictionary RequestTrailers { get; } = new HeaderDictionary();
        public bool RequestTrailersAvailable { get; set; }
        public Stream RequestBody { get; set; } = default!;
        public PipeReader RequestBodyPipeReader { get; set; } = default!;
        public HttpResponseTrailers? ResponseTrailers { get; set; }

        private int _statusCode;
        public int StatusCode
        {
            get => _statusCode;
            set
            {
                if (HasResponseStarted)
                {
                    ThrowResponseAlreadyStartedException(nameof(StatusCode));
                }

                _statusCode = value;
            }
        }

        private string? _reasonPhrase;

        public string? ReasonPhrase
        {
            get => _reasonPhrase;

            set
            {
                if (HasResponseStarted)
                {
                    ThrowResponseAlreadyStartedException(nameof(ReasonPhrase));
                }

                _reasonPhrase = value;
            }
        }

        public IHeaderDictionary ResponseHeaders { get; set; } = default!;
        public Stream ResponseBody { get; set; } = default!;
        public PipeWriter ResponseBodyPipeWriter { get; set; } = default!;

        public CancellationToken RequestAborted
        {
            get
            {
                // If a request abort token was previously explicitly set, return it.
                if (_manuallySetRequestAbortToken.HasValue)
                {
                    return _manuallySetRequestAbortToken.Value;
                }

                lock (_abortLock)
                {
                    if (_preventRequestAbortedCancellation)
                    {
                        return new CancellationToken(false);
                    }

                    if (_connectionAborted)
                    {
                        return new CancellationToken(true);
                    }

                    if (_abortedCts == null)
                    {
                        _abortedCts = new CancellationTokenSource();
                    }

                    return _abortedCts.Token;
                }
            }
            set
            {
                // Set an abort token, overriding one we create internally.  This setter and associated
                // field exist purely to support IHttpRequestLifetimeFeature.set_RequestAborted.
                _manuallySetRequestAbortToken = value;
            }
        }

        public bool HasResponseStarted => _requestProcessingStatus >= RequestProcessingStatus.HeadersCommitted;

        public bool HasFlushedHeaders => _requestProcessingStatus >= RequestProcessingStatus.HeadersFlushed;

        public bool HasResponseCompleted => _requestProcessingStatus == RequestProcessingStatus.ResponseCompleted;

        protected HttpRequestHeaders HttpRequestHeaders { get; set; } = new HttpRequestHeaders();

        protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders();

        public void InitializeBodyControl(MessageBody messageBody)
        {
            if (_bodyControl == null)
            {
                _bodyControl = new BodyControl(bodyControl: this, this);
            }

            (RequestBody, ResponseBody, RequestBodyPipeReader, ResponseBodyPipeWriter) = _bodyControl.Start(messageBody);
            _requestStreamInternal = RequestBody;
            _responseStreamInternal = ResponseBody;
        }

        // For testing
        internal void ResetState()
        {
            _requestProcessingStatus = RequestProcessingStatus.RequestPending;
        }

        public void Reset()
        {
            _onStarting?.Clear();
            _onCompleted?.Clear();
            _routeValues?.Clear();

            _requestProcessingStatus = RequestProcessingStatus.RequestPending;
            _autoChunk = false;
            _applicationException = null;
            _requestRejectedException = null;

            ResetFeatureCollection();

            HasStartedConsumingRequestBody = false;
            MaxRequestBodySize = ServerOptions.Limits.MaxRequestBodySize;
            MinRequestBodyDataRate = ServerOptions.Limits.MinRequestBodyDataRate;
            AllowSynchronousIO = ServerOptions.AllowSynchronousIO;
            TraceIdentifier = null!;
            Method = HttpMethod.None;
            _methodText = null;
            _endpoint = null;
            PathBase = null;
            Path = null;
            RawTarget = null;
            QueryString = null;
            _httpVersion = Http.HttpVersion.Unknown;
            _httpProtocol = null;
            _statusCode = StatusCodes.Status200OK;
            _reasonPhrase = null;

            var remoteEndPoint = RemoteEndPoint;
            RemoteIpAddress = remoteEndPoint?.Address;
            RemotePort = remoteEndPoint?.Port ?? 0;
            var localEndPoint = LocalEndPoint;
            LocalIpAddress = localEndPoint?.Address;
            LocalPort = localEndPoint?.Port ?? 0;

            ConnectionIdFeature = ConnectionId;

            HttpRequestHeaders.Reset();
            HttpRequestHeaders.EncodingSelector = ServerOptions.RequestHeaderEncodingSelector;
            HttpRequestHeaders.ReuseHeaderValues = !ServerOptions.DisableStringReuse;
            HttpResponseHeaders.Reset();
            RequestHeaders = HttpRequestHeaders;
            ResponseHeaders = HttpResponseHeaders;
            RequestTrailers.Clear();
            ResponseTrailers?.Reset();
            RequestTrailersAvailable = false;

            _isLeasedMemoryInvalid = true;
            _hasAdvanced = false;
            _canWriteResponseBody = true;

            if (_scheme == null)
            {
                var tlsFeature = ConnectionFeatures?[typeof(ITlsConnectionFeature)];
                _scheme = tlsFeature != null ? SchemeHttps : SchemeHttp;
            }

            Scheme = _scheme;

            _manuallySetRequestAbortToken = null;

            // Lock to prevent CancelRequestAbortedToken from attempting to cancel a disposed CTS.
            CancellationTokenSource? localAbortCts = null;

            lock (_abortLock)
            {
                _preventRequestAbortedCancellation = false;
                if (_abortedCts?.TryReset() == false)
                {
                    localAbortCts = _abortedCts;
                    _abortedCts = null;
                }
            }

            localAbortCts?.Dispose();

            Output?.Reset();

            _requestHeadersParsed = 0;

            _responseBytesWritten = 0;

            OnReset();
        }

        protected abstract void OnReset();

        protected abstract void ApplicationAbort();

        protected virtual void OnRequestProcessingEnding()
        {
        }

        protected virtual void OnRequestProcessingEnded()
        {
        }

        protected virtual void BeginRequestProcessing()
        {
        }

        protected virtual void OnErrorAfterResponseStarted()
        {
        }

        protected virtual bool BeginRead(out ValueTask<ReadResult> awaitable)
        {
            awaitable = default;
            return false;
        }

        protected abstract string CreateRequestId();

        protected abstract MessageBody CreateMessageBody();

        protected abstract bool TryParseRequest(ReadResult result, out bool endConnection);

        private void CancelRequestAbortedTokenCallback()
        {
            try
            {
                CancellationTokenSource? localAbortCts = null;

                lock (_abortLock)
                {
                    if (_abortedCts != null && !_preventRequestAbortedCancellation)
                    {
                        localAbortCts = _abortedCts;
                        _abortedCts = null;
                    }
                }

                // If we cancel the cts, we don't dispose as people may still be using
                // the cts. It also isn't necessary to dispose a canceled cts.
                localAbortCts?.Cancel();
            }
            catch (Exception ex)
            {
                Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
            }
        }

        protected void CancelRequestAbortedToken()
        {
            var shouldScheduleCancellation = false;

            lock (_abortLock)
            {
                if (_connectionAborted)
                {
                    return;
                }

                shouldScheduleCancellation = _abortedCts != null && !_preventRequestAbortedCancellation;
                _connectionAborted = true;
            }

            if (shouldScheduleCancellation)
            {
                // Potentially calling user code. CancelRequestAbortedToken logs any exceptions.
                ServiceContext.Scheduler.Schedule(state => ((HttpProtocol)state!).CancelRequestAbortedTokenCallback(), this);
            }
        }

        protected void PoisonBody(Exception abortReason)
        {
            _bodyControl?.Abort(abortReason);
        }

        // Prevents the RequestAborted token from firing for the duration of the request.
        private void PreventRequestAbortedCancellation()
        {
            lock (_abortLock)
            {
                if (_connectionAborted)
                {
                    return;
                }

                _preventRequestAbortedCancellation = true;
            }
        }

        public virtual void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
        {
            IncrementRequestHeadersCount();

            HttpRequestHeaders.Append(name, value);
        }

        public virtual void OnHeader(int index, bool indexOnly, ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
        {
            IncrementRequestHeadersCount();

            // This method should be overriden in specific implementations and the base should be
            // called to validate the header count.
        }

        public void OnTrailer(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
        {
            IncrementRequestHeadersCount();

            string key = name.GetHeaderName();
            var valueStr = value.GetRequestHeaderString(key, HttpRequestHeaders.EncodingSelector);
            RequestTrailers.Append(key, valueStr);
        }

        private void IncrementRequestHeadersCount()
        {
            _requestHeadersParsed++;
            if (_requestHeadersParsed > ServerOptions.Limits.MaxRequestHeaderCount)
            {
                KestrelBadHttpRequestException.Throw(RequestRejectionReason.TooManyHeaders);
            }
        }

        public void OnHeadersComplete()
        {
            HttpRequestHeaders.OnHeadersComplete();
        }

        public void OnTrailersComplete()
        {
            RequestTrailersAvailable = true;
        }

        public async Task ProcessRequestsAsync<TContext>(IHttpApplication<TContext> application) where TContext : notnull
        {
            try
            {
                // We run the request processing loop in a seperate async method so per connection
                // exception handling doesn't complicate the generated asm for the loop.
                await ProcessRequests(application);
            }
            catch (BadHttpRequestException ex)
            {
                // Handle BadHttpRequestException thrown during request line or header parsing.
                // SetBadRequestState logs the error.
                SetBadRequestState(ex);
            }
            catch (ConnectionResetException ex)
            {
                // Don't log ECONNRESET errors made between requests. Browsers like IE will reset connections regularly.
                if (_requestProcessingStatus != RequestProcessingStatus.RequestPending)
                {
                    Log.RequestProcessingError(ConnectionId, ex);
                }
            }
            catch (IOException ex)
            {
                Log.RequestProcessingError(ConnectionId, ex);
            }
            catch (ConnectionAbortedException ex)
            {
                Log.RequestProcessingError(ConnectionId, ex);
            }
            catch (Exception ex)
            {
                Log.LogWarning(0, ex, CoreStrings.RequestProcessingEndError);
            }
            finally
            {
                try
                {
                    await TryProduceInvalidRequestResponse();
                }
                catch (Exception ex)
                {
                    Log.LogWarning(0, ex, CoreStrings.ConnectionShutdownError);
                }
                finally
                {
                    OnRequestProcessingEnded();
                }
            }
        }

        private async Task ProcessRequests<TContext>(IHttpApplication<TContext> application) where TContext : notnull
        {
            while (_keepAlive)
            {
                if (_context.InitialExecutionContext is null)
                {
                    // If this is a first request on a non-Http2Connection, capture a clean ExecutionContext.
                    _context.InitialExecutionContext = ExecutionContext.Capture();
                }
                else
                {
                    // Clear any AsyncLocals set during the request; back to a clean state ready for next request
                    // And/or reset to Http2Connection's ExecutionContext giving access to the connection logging scope
                    // and any other AsyncLocals set by connection middleware.
                    ExecutionContext.Restore(_context.InitialExecutionContext);
                }

                BeginRequestProcessing();

                var result = default(ReadResult);
                bool endConnection;
                do
                {
                    if (BeginRead(out var awaitable))
                    {
                        result = await awaitable;
                    }
                } while (!TryParseRequest(result, out endConnection));

                if (endConnection)
                {
                    // Connection finished, stop processing requests
                    return;
                }

                var messageBody = CreateMessageBody();
                if (!messageBody.RequestKeepAlive)
                {
                    _keepAlive = false;
                }

                IsUpgradableRequest = messageBody.RequestUpgrade;

                InitializeBodyControl(messageBody);


                var context = application.CreateContext(this);

                try
                {
                    KestrelEventSource.Log.RequestStart(this);

                    // Run the application code for this request
                    await application.ProcessRequestAsync(context);

                    // Trigger OnStarting if it hasn't been called yet and the app hasn't
                    // already failed. If an OnStarting callback throws we can go through
                    // our normal error handling in ProduceEnd.
                    // https://github.com/aspnet/KestrelHttpServer/issues/43
                    if (!HasResponseStarted && _applicationException == null && _onStarting?.Count > 0)
                    {
                        await FireOnStarting();
                    }

                    if (!_connectionAborted && !VerifyResponseContentLength(out var lengthException))
                    {
                        ReportApplicationError(lengthException);
                    }
                }
                catch (BadHttpRequestException ex)
                {
                    // Capture BadHttpRequestException for further processing
                    // This has to be caught here so StatusCode is set properly before disposing the HttpContext
                    // (DisposeContext logs StatusCode).
                    SetBadRequestState(ex);
                    ReportApplicationError(ex);
                }
                catch (Exception ex)
                {
                    ReportApplicationError(ex);
                }

                KestrelEventSource.Log.RequestStop(this);

                // At this point all user code that needs use to the request or response streams has completed.
                // Using these streams in the OnCompleted callback is not allowed.
                try
                {
                    Debug.Assert(_bodyControl != null);
                    await _bodyControl.StopAsync();
                }
                catch (Exception ex)
                {
                    // BodyControl.StopAsync() can throw if the PipeWriter was completed prior to the application writing
                    // enough bytes to satisfy the specified Content-Length. This risks double-logging the exception,
                    // but this scenario generally indicates an app bug, so I don't want to risk not logging it.
                    ReportApplicationError(ex);
                }

                // 4XX responses are written by TryProduceInvalidRequestResponse during connection tear down.
                if (_requestRejectedException == null)
                {
                    if (!_connectionAborted)
                    {
                        // Call ProduceEnd() before consuming the rest of the request body to prevent
                        // delaying clients waiting for the chunk terminator:
                        //
                        // https://github.com/dotnet/corefx/issues/17330#issuecomment-288248663
                        //
                        // This also prevents the 100 Continue response from being sent if the app
                        // never tried to read the body.
                        // https://github.com/aspnet/KestrelHttpServer/issues/2102
                        //
                        // ProduceEnd() must be called before _application.DisposeContext(), to ensure
                        // HttpContext.Response.StatusCode is correctly set when
                        // IHttpContextFactory.Dispose(HttpContext) is called.
                        await ProduceEnd();
                    }
                    else if (!HasResponseStarted)
                    {
                        // If the request was aborted and no response was sent, there's no
                        // meaningful status code to log.
                        StatusCode = 0;
                    }
                }

                if (_onCompleted?.Count > 0)
                {
                    await FireOnCompleted();
                }

                application.DisposeContext(context, _applicationException);

                // Even for non-keep-alive requests, try to consume the entire body to avoid RSTs.
                if (!_connectionAborted && _requestRejectedException == null && !messageBody.IsEmpty)
                {
                    await messageBody.ConsumeAsync();
                }

                if (HasStartedConsumingRequestBody)
                {
                    await messageBody.StopAsync();
                }
            }
        }

        public void OnStarting(Func<object, Task> callback, object state)
        {
            if (HasResponseStarted)
            {
                ThrowResponseAlreadyStartedException(nameof(OnStarting));
            }

            if (_onStarting == null)
            {
                _onStarting = new Stack<KeyValuePair<Func<object, Task>, object>>();
            }
            _onStarting.Push(new KeyValuePair<Func<object, Task>, object>(callback, state));
        }

        public void OnCompleted(Func<object, Task> callback, object state)
        {
            if (_onCompleted == null)
            {
                _onCompleted = new Stack<KeyValuePair<Func<object, Task>, object>>();
            }
            _onCompleted.Push(new KeyValuePair<Func<object, Task>, object>(callback, state));
        }

        protected Task FireOnStarting()
        {
            var onStarting = _onStarting;
            if (onStarting?.Count > 0)
            {
                return ProcessEvents(this, onStarting);
            }

            return Task.CompletedTask;

            static async Task ProcessEvents(HttpProtocol protocol, Stack<KeyValuePair<Func<object, Task>, object>> events)
            {
                // Try/Catch is outside the loop as any error that occurs is before the request starts.
                // So we want to report it as an ApplicationError to fail the request and not process more events.
                try
                {
                    while (events.TryPop(out var entry))
                    {
                        await entry.Key.Invoke(entry.Value);
                    }
                }
                catch (Exception ex)
                {
                    protocol.ReportApplicationError(ex);
                }
            }
        }

        protected Task FireOnCompleted()
        {
            var onCompleted = _onCompleted;
            if (onCompleted?.Count > 0)
            {
                return ProcessEvents(this, onCompleted);
            }

            return Task.CompletedTask;

            static async Task ProcessEvents(HttpProtocol protocol, Stack<KeyValuePair<Func<object, Task>, object>> events)
            {
                // Try/Catch is inside the loop as any error that occurs is after the request has finished.
                // So we will just log it and keep processing the events, as the completion has already happened.
                while (events.TryPop(out var entry))
                {
                    try
                    {
                        await entry.Key.Invoke(entry.Value);
                    }
                    catch (Exception ex)
                    {
                        protocol.Log.ApplicationError(protocol.ConnectionId, protocol.TraceIdentifier, ex);
                    }
                }
            }
        }

        private void VerifyAndUpdateWrite(int count)
        {
            var responseHeaders = HttpResponseHeaders;

            if (responseHeaders != null &&
                !responseHeaders.HasTransferEncoding &&
                responseHeaders.ContentLength.HasValue &&
                _responseBytesWritten + count > responseHeaders.ContentLength.Value)
            {
                _keepAlive = false;
                ThrowTooManyBytesWritten(count);
            }

            _responseBytesWritten += count;
        }

        [StackTraceHidden]
        private void ThrowTooManyBytesWritten(int count)
        {
            throw GetTooManyBytesWrittenException(count);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private InvalidOperationException GetTooManyBytesWrittenException(int count)
        {
            var responseHeaders = HttpResponseHeaders;
            return new InvalidOperationException(
                CoreStrings.FormatTooManyBytesWritten(_responseBytesWritten + count, responseHeaders.ContentLength!.Value));
        }

        private void CheckLastWrite()
        {
            var responseHeaders = HttpResponseHeaders;

            // Prevent firing request aborted token if this is the last write, to avoid
            // aborting the request if the app is still running when the client receives
            // the final bytes of the response and gracefully closes the connection.
            //
            // Called after VerifyAndUpdateWrite(), so _responseBytesWritten has already been updated.
            if (responseHeaders != null &&
                !responseHeaders.HasTransferEncoding &&
                responseHeaders.ContentLength.HasValue &&
                _responseBytesWritten == responseHeaders.ContentLength.Value)
            {
                PreventRequestAbortedCancellation();
            }
        }

        protected bool VerifyResponseContentLength([NotNullWhen(false)] out Exception? ex)
        {
            var responseHeaders = HttpResponseHeaders;

            if (Method != HttpMethod.Head &&
                StatusCode != StatusCodes.Status304NotModified &&
                !responseHeaders.HasTransferEncoding &&
                responseHeaders.ContentLength.HasValue &&
                _responseBytesWritten < responseHeaders.ContentLength.Value)
            {
                // We need to close the connection if any bytes were written since the client
                // cannot be certain of how many bytes it will receive.
                if (_responseBytesWritten > 0)
                {
                    _keepAlive = false;
                }

                ex = new InvalidOperationException(
                    CoreStrings.FormatTooFewBytesWritten(_responseBytesWritten, responseHeaders.ContentLength.Value));
                return false;
            }

            ex = null;
            return true;
        }

        public void ProduceContinue()
        {
            if (HasResponseStarted)
            {
                return;
            }

            if (_httpVersion != Http.HttpVersion.Http10 &&
                ((IHeaderDictionary)HttpRequestHeaders).TryGetValue(HeaderNames.Expect, out var expect) &&
                (expect.FirstOrDefault() ?? "").Equals("100-continue", StringComparison.OrdinalIgnoreCase))
            {
                ValueTask<FlushResult> vt = Output.Write100ContinueAsync();
                if (vt.IsCompleted)
                {
                    vt.GetAwaiter().GetResult();
                }
                else
                {
                    vt.AsTask().GetAwaiter().GetResult();
                }
            }
        }

        public Task InitializeResponseAsync(int firstWriteByteCount)
        {
            var startingTask = FireOnStarting();
            if (!startingTask.IsCompletedSuccessfully)
            {
                return InitializeResponseAwaited(startingTask, firstWriteByteCount);
            }

            VerifyInitializeState(firstWriteByteCount);

            ProduceStart(appCompleted: false);

            return Task.CompletedTask;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public async Task InitializeResponseAwaited(Task startingTask, int firstWriteByteCount)
        {
            await startingTask;

            VerifyInitializeState(firstWriteByteCount);

            ProduceStart(appCompleted: false);
        }

        private HttpResponseHeaders InitializeResponseFirstWrite(int firstWriteByteCount)
        {
            VerifyInitializeState(firstWriteByteCount);

            var responseHeaders = CreateResponseHeaders(appCompleted: false);

            // InitializeResponse can only be called if we are just about to Flush the headers
            _requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;

            return responseHeaders;
        }

        private void ProduceStart(bool appCompleted)
        {
            if (HasResponseStarted)
            {
                return;
            }

            _isLeasedMemoryInvalid = true;

            _requestProcessingStatus = RequestProcessingStatus.HeadersCommitted;

            var responseHeaders = CreateResponseHeaders(appCompleted);

            Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, appCompleted);
        }

        private void VerifyInitializeState(int firstWriteByteCount)
        {
            if (_applicationException != null)
            {
                ThrowResponseAbortedException();
            }

            VerifyAndUpdateWrite(firstWriteByteCount);
        }

        protected Task TryProduceInvalidRequestResponse()
        {
            // If _requestAborted is set, the connection has already been closed.
            if (_requestRejectedException != null && !_connectionAborted)
            {
                return ProduceEnd();
            }

            return Task.CompletedTask;
        }

        protected Task ProduceEnd()
        {
            if (HasResponseCompleted)
            {
                return Task.CompletedTask;
            }

            _isLeasedMemoryInvalid = true;

            if (_requestRejectedException != null || _applicationException != null)
            {
                if (HasResponseStarted)
                {
                    // We can no longer change the response, so we simply close the connection.
                    _keepAlive = false;
                    OnErrorAfterResponseStarted();
                    return Task.CompletedTask;
                }

                // If the request was rejected, the error state has already been set by SetBadRequestState and
                // that should take precedence.
                if (_requestRejectedException != null)
                {
                    SetErrorResponseException(_requestRejectedException);
                }
                else
                {
                    // 500 Internal Server Error
                    SetErrorResponseHeaders(statusCode: StatusCodes.Status500InternalServerError);
                }
            }

            if (!HasResponseStarted)
            {
                ProduceStart(appCompleted: true);
            }

            return WriteSuffix();
        }

        private Task WriteSuffix()
        {
            if (_autoChunk || _httpVersion >= Http.HttpVersion.Http2)
            {
                // For the same reason we call CheckLastWrite() in Content-Length responses.
                PreventRequestAbortedCancellation();
            }

            var writeTask = Output.WriteStreamSuffixAsync();

            if (!writeTask.IsCompletedSuccessfully)
            {
                return WriteSuffixAwaited(writeTask);
            }

            writeTask.GetAwaiter().GetResult();

            _requestProcessingStatus = RequestProcessingStatus.ResponseCompleted;

            if (_keepAlive)
            {
                Log.ConnectionKeepAlive(ConnectionId);
            }

            if (Method == HttpMethod.Head && _responseBytesWritten > 0)
            {
                Log.ConnectionHeadResponseBodyWrite(ConnectionId, _responseBytesWritten);
            }

            return Task.CompletedTask;
        }

        private async Task WriteSuffixAwaited(ValueTask<FlushResult> writeTask)
        {
            _requestProcessingStatus = RequestProcessingStatus.HeadersFlushed;

            await writeTask;

            _requestProcessingStatus = RequestProcessingStatus.ResponseCompleted;

            if (_keepAlive)
            {
                Log.ConnectionKeepAlive(ConnectionId);
            }

            if (Method == HttpMethod.Head && _responseBytesWritten > 0)
            {
                Log.ConnectionHeadResponseBodyWrite(ConnectionId, _responseBytesWritten);
            }
        }

        private HttpResponseHeaders CreateResponseHeaders(bool appCompleted)
        {
            var responseHeaders = HttpResponseHeaders;
            var hasConnection = responseHeaders.HasConnection;
            var hasTransferEncoding = responseHeaders.HasTransferEncoding;

            // We opt to remove the following headers from an HTTP/2+ response since their presence would be considered a protocol violation.
            // This is done quietly because these headers are valid in other contexts and this saves the app from being broken by
            // low level protocol details. Http.Sys also removes these headers silently.
            //
            // https://tools.ietf.org/html/rfc7540#section-8.1.2.2
            // "This means that an intermediary transforming an HTTP/1.x message to HTTP/2 will need to remove any header fields
            // nominated by the Connection header field, along with the Connection header field itself.
            // Such intermediaries SHOULD also remove other connection-specific header fields, such as Keep-Alive,
            // Proxy-Connection, Transfer-Encoding, and Upgrade, even if they are not nominated by the Connection header field."
            //
            // Http/3 has a similar requirement: https://quicwg.org/base-drafts/draft-ietf-quic-http.html#name-field-formatting-and-compre
            if (_httpVersion > Http.HttpVersion.Http11 && responseHeaders.HasInvalidH2H3Headers)
            {
                responseHeaders.ClearInvalidH2H3Headers();
                hasTransferEncoding = false;
                hasConnection = false;

                Log.InvalidResponseHeaderRemoved();
            }

            if (_keepAlive &&
                hasConnection &&
                (HttpHeaders.ParseConnection(responseHeaders) & ConnectionOptions.KeepAlive) == 0)
            {
                _keepAlive = false;
            }

            // https://tools.ietf.org/html/rfc7230#section-3.3.1
            // If any transfer coding other than
            // chunked is applied to a response payload body, the sender MUST either
            // apply chunked as the final transfer coding or terminate the message
            // by closing the connection.
            if (hasTransferEncoding &&
                HttpHeaders.GetFinalTransferCoding(responseHeaders.HeaderTransferEncoding) != TransferCoding.Chunked)
            {
                _keepAlive = false;
            }

            // Set whether response can have body
            _canWriteResponseBody = CanWriteResponseBody();

            if (!_canWriteResponseBody && hasTransferEncoding)
            {
                RejectNonBodyTransferEncodingResponse(appCompleted);
            }
            else if (StatusCode == StatusCodes.Status101SwitchingProtocols)
            {
                _keepAlive = false;
            }
            else if (!hasTransferEncoding && !responseHeaders.ContentLength.HasValue)
            {
                if ((appCompleted || !_canWriteResponseBody) && !_hasAdvanced) // Avoid setting contentLength of 0 if we wrote data before calling CreateResponseHeaders
                {
                    // Don't set the Content-Length header automatically for HEAD requests, 204 responses, or 304 responses.
                    if (CanAutoSetContentLengthZeroResponseHeader())
                    {
                        // Since the app has completed writing or cannot write to the response, we can safely set the Content-Length to 0.
                        responseHeaders.ContentLength = 0;
                    }
                }
                // Note for future reference: never change this to set _autoChunk to true on HTTP/1.0
                // connections, even if we were to infer the client supports it because an HTTP/1.0 request
                // was received that used chunked encoding. Sending a chunked response to an HTTP/1.0
                // client would break compliance with RFC 7230 (section 3.3.1):
                //
                // A server MUST NOT send a response containing Transfer-Encoding unless the corresponding
                // request indicates HTTP/1.1 (or later).
                //
                // This also covers HTTP/2, which forbids chunked encoding in RFC 7540 (section 8.1:
                //
                // The chunked transfer encoding defined in Section 4.1 of [RFC7230] MUST NOT be used in HTTP/2.
                else if (_httpVersion == Http.HttpVersion.Http11)
                {
                    _autoChunk = true;
                    responseHeaders.SetRawTransferEncoding("chunked", _bytesTransferEncodingChunked);
                }
                else
                {
                    _keepAlive = false;
                }
            }

            responseHeaders.SetReadOnly();

            if (!hasConnection && _httpVersion < Http.HttpVersion.Http2)
            {
                if (!_keepAlive)
                {
                    responseHeaders.SetRawConnection("close", _bytesConnectionClose);
                }
                else if (_httpVersion == Http.HttpVersion.Http10)
                {
                    responseHeaders.SetRawConnection("keep-alive", _bytesConnectionKeepAlive);
                }
            }

            // TODO allow customization of this.
            // Chrome is using h3-29 for protocol ID as of 12/2020. This is likely to be the alt-svc
            // value until HTTP/3 is finalized.
            // More info: https://blog.chromium.org/2020/10/chrome-is-deploying-http3-and-ietf-quic.html
            if (ServerOptions.EnableAltSvc && _httpVersion < Http.HttpVersion.Http3)
            {
                foreach (var option in ServerOptions.ListenOptions)
                {
                    if ((option.Protocols & HttpProtocols.Http3) == HttpProtocols.Http3)
                    {
                        responseHeaders.HeaderAltSvc = $"h3-29=\":{option.IPEndPoint!.Port}\"; ma=84600";
                        break;
                    }
                }
            }

            if (ServerOptions.AddServerHeader && !responseHeaders.HasServer)
            {
                responseHeaders.SetRawServer(Constants.ServerName, _bytesServer);
            }

            if (!responseHeaders.HasDate)
            {
                var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();
                responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);
            }

            return responseHeaders;
        }

        private bool CanWriteResponseBody()
        {
            // List of status codes taken from Microsoft.Net.Http.Server.Response
            return Method != HttpMethod.Head &&
                   StatusCode != StatusCodes.Status204NoContent &&
                   StatusCode != StatusCodes.Status205ResetContent &&
                   StatusCode != StatusCodes.Status304NotModified;
        }

        private bool CanAutoSetContentLengthZeroResponseHeader()
        {
            return Method != HttpMethod.Head &&
                   StatusCode != StatusCodes.Status204NoContent &&
                   StatusCode != StatusCodes.Status304NotModified;
        }

        private static void ThrowResponseAlreadyStartedException(string value)
        {
            throw new InvalidOperationException(CoreStrings.FormatParameterReadOnlyAfterResponseStarted(value));
        }

        private void RejectNonBodyTransferEncodingResponse(bool appCompleted)
        {
            var ex = new InvalidOperationException(CoreStrings.FormatHeaderNotAllowedOnResponse("Transfer-Encoding", StatusCode));
            if (!appCompleted)
            {
                // Back out of header creation surface exception in user code
                _requestProcessingStatus = RequestProcessingStatus.AppStarted;
                throw ex;
            }
            else
            {
                ReportApplicationError(ex);

                // 500 Internal Server Error
                SetErrorResponseHeaders(statusCode: StatusCodes.Status500InternalServerError);
            }
        }

        private void SetErrorResponseException(BadHttpRequestException ex)
        {
            SetErrorResponseHeaders(ex.StatusCode);

#pragma warning disable CS0618 // Type or member is obsolete
            if (ex is Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException kestrelEx && !StringValues.IsNullOrEmpty(kestrelEx.AllowedHeader))
#pragma warning restore CS0618 // Type or member is obsolete
            {
                HttpResponseHeaders.HeaderAllow = kestrelEx.AllowedHeader;
            }
        }

        private void SetErrorResponseHeaders(int statusCode)
        {
            Debug.Assert(!HasResponseStarted, $"{nameof(SetErrorResponseHeaders)} called after response had already started.");

            StatusCode = statusCode;
            ReasonPhrase = null;

            var responseHeaders = HttpResponseHeaders;
            responseHeaders.Reset();
            ResponseTrailers?.Reset();
            var dateHeaderValues = DateHeaderValueManager.GetDateHeaderValues();

            responseHeaders.SetRawDate(dateHeaderValues.String, dateHeaderValues.Bytes);

            responseHeaders.ContentLength = 0;

            if (ServerOptions.AddServerHeader)
            {
                responseHeaders.SetRawServer(Constants.ServerName, _bytesServer);
            }
        }

        public void HandleNonBodyResponseWrite()
        {
            // Writes to HEAD response are ignored and logged at the end of the request
            if (Method != HttpMethod.Head)
            {
                ThrowWritingToResponseBodyNotSupported();
            }
        }

        [StackTraceHidden]
        private void ThrowWritingToResponseBodyNotSupported()
        {
            // Throw Exception for 204, 205, 304 responses.
            throw new InvalidOperationException(CoreStrings.FormatWritingToResponseBodyNotSupported(StatusCode));
        }

        [StackTraceHidden]
        private void ThrowResponseAbortedException()
        {
            throw new ObjectDisposedException(CoreStrings.UnhandledApplicationException, _applicationException);
        }

        [StackTraceHidden]
        [DoesNotReturn]
        public void ThrowRequestTargetRejected(Span<byte> target)
            => throw GetInvalidRequestTargetException(target);

        [MethodImpl(MethodImplOptions.NoInlining)]
        private BadHttpRequestException GetInvalidRequestTargetException(ReadOnlySpan<byte> target)
            => KestrelBadHttpRequestException.GetException(
                RequestRejectionReason.InvalidRequestTarget,
                Log.IsEnabled(LogLevel.Information)
                    ? target.GetAsciiStringEscaped(Constants.MaxExceptionDetailSize)
                    : string.Empty);

        public void SetBadRequestState(BadHttpRequestException ex)
        {
            Log.ConnectionBadRequest(ConnectionId, ex);

            if (!HasResponseStarted)
            {
                SetErrorResponseException(ex);
            }

            _keepAlive = false;
            _requestRejectedException = ex;
        }

        public void ReportApplicationError(Exception? ex)
        {
            // ReportApplicationError can be called with a null exception from MessageBody
            if (ex == null)
            {
                return;
            }

            if (_applicationException == null)
            {
                _applicationException = ex;
            }
            else if (_applicationException is AggregateException)
            {
                _applicationException = new AggregateException(_applicationException, ex).Flatten();
            }
            else
            {
                _applicationException = new AggregateException(_applicationException, ex);
            }

            Log.ApplicationError(ConnectionId, TraceIdentifier, ex);
        }

        public void Advance(int bytes)
        {
            if (bytes < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(bytes));
            }
            else if (bytes > 0)
            {
                _hasAdvanced = true;
            }

            if (_isLeasedMemoryInvalid)
            {
                throw new InvalidOperationException("Invalid ordering of calling StartAsync or CompleteAsync and Advance.");
            }

            if (_canWriteResponseBody)
            {
                VerifyAndUpdateWrite(bytes);
                Output.Advance(bytes);
            }
            else
            {
                HandleNonBodyResponseWrite();
                // For HEAD requests, we still use the number of bytes written for logging
                // how many bytes were written.
                VerifyAndUpdateWrite(bytes);
            }
        }

        public Memory<byte> GetMemory(int sizeHint = 0)
        {
            _isLeasedMemoryInvalid = false;
            return Output.GetMemory(sizeHint);
        }

        public Span<byte> GetSpan(int sizeHint = 0)
        {
            _isLeasedMemoryInvalid = false;
            return Output.GetSpan(sizeHint);
        }

        public ValueTask<FlushResult> FlushPipeAsync(CancellationToken cancellationToken)
        {
            if (!HasResponseStarted)
            {
                var initializeTask = InitializeResponseAsync(0);
                if (!initializeTask.IsCompletedSuccessfully)
                {
                    return FlushAsyncAwaited(initializeTask, cancellationToken);
                }
            }

            return Output.FlushAsync(cancellationToken);
        }

        public void CancelPendingFlush()
        {
            Output.CancelPendingFlush();
        }

        public Task CompleteAsync(Exception? exception = null)
        {
            if (exception != null)
            {
                var wrappedException = new ConnectionAbortedException("The BodyPipe was completed with an exception.", exception);
                ReportApplicationError(wrappedException);

                if (HasResponseStarted)
                {
                    ApplicationAbort();
                }
            }

            // Finalize headers
            if (!HasResponseStarted)
            {
                var onStartingTask = FireOnStarting();
                if (!onStartingTask.IsCompletedSuccessfully)
                {
                    return CompleteAsyncAwaited(onStartingTask);
                }
            }

            // Flush headers, body, trailers...
            if (!HasResponseCompleted)
            {
                if (!VerifyResponseContentLength(out var lengthException))
                {
                    // Try to throw this exception from CompleteAsync() instead of CompleteAsyncAwaited() if possible,
                    // so it can be observed by BodyWriter.Complete(). If this isn't possible because an
                    // async OnStarting callback hadn't yet run, it's OK, since the Exception will be observed with
                    // the call to _bodyControl.StopAsync() in ProcessRequests().
                    ThrowException(lengthException);
                }

                return ProduceEnd();
            }

            return Task.CompletedTask;
        }

        private async Task CompleteAsyncAwaited(Task onStartingTask)
        {
            await onStartingTask;

            if (!HasResponseCompleted)
            {
                if (!VerifyResponseContentLength(out var lengthException))
                {
                    ThrowException(lengthException);
                }

                await ProduceEnd();
            }
        }

        [StackTraceHidden]
        private static void ThrowException(Exception exception)
        {
            throw exception;
        }

        public ValueTask<FlushResult> WritePipeAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            // For the first write, ensure headers are flushed if WriteDataAsync isn't called.
            if (!HasResponseStarted)
            {
                return FirstWriteAsync(data, cancellationToken);
            }
            else
            {
                VerifyAndUpdateWrite(data.Length);
            }

            if (_canWriteResponseBody)
            {
                if (_autoChunk)
                {
                    if (data.Length == 0)
                    {
                        return default;
                    }

                    return Output.WriteChunkAsync(data.Span, cancellationToken);
                }
                else
                {
                    CheckLastWrite();
                    return Output.WriteDataToPipeAsync(data.Span, cancellationToken: cancellationToken);
                }
            }
            else
            {
                HandleNonBodyResponseWrite();
                return default;
            }
        }

        private ValueTask<FlushResult> FirstWriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            Debug.Assert(!HasResponseStarted);

            var startingTask = FireOnStarting();
            if (!startingTask.IsCompletedSuccessfully)
            {
                return FirstWriteAsyncAwaited(startingTask, data, cancellationToken);
            }

            return FirstWriteAsyncInternal(data, cancellationToken);
        }

        private async ValueTask<FlushResult> FirstWriteAsyncAwaited(Task initializeTask, ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            await initializeTask;

            return await FirstWriteAsyncInternal(data, cancellationToken);
        }

        private ValueTask<FlushResult> FirstWriteAsyncInternal(ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            var responseHeaders = InitializeResponseFirstWrite(data.Length);

            if (_canWriteResponseBody)
            {
                if (_autoChunk)
                {
                    if (data.Length == 0)
                    {
                        Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, appCompleted: false);
                        return Output.FlushAsync(cancellationToken);
                    }

                    return Output.FirstWriteChunkedAsync(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, data.Span, cancellationToken);
                }
                else
                {
                    CheckLastWrite();
                    return Output.FirstWriteAsync(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, data.Span, cancellationToken);
                }
            }
            else
            {
                Output.WriteResponseHeaders(StatusCode, ReasonPhrase, responseHeaders, _autoChunk, appCompleted: false);
                HandleNonBodyResponseWrite();
                return Output.FlushAsync(cancellationToken);
            }
        }

        public Task FlushAsync(CancellationToken cancellationToken = default)
        {
            return FlushPipeAsync(cancellationToken).GetAsTask();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private async ValueTask<FlushResult> FlushAsyncAwaited(Task initializeTask, CancellationToken cancellationToken)
        {
            await initializeTask;
            return await Output.FlushAsync(cancellationToken);
        }

        public Task WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)
        {
            return WritePipeAsync(data, cancellationToken).GetAsTask();
        }

        public async ValueTask<FlushResult> WriteAsyncAwaited(Task initializeTask, ReadOnlyMemory<byte> data, CancellationToken cancellationToken)
        {
            await initializeTask;

            // WriteAsyncAwaited is only called for the first write to the body.
            // Ensure headers are flushed if Write(Chunked)Async isn't called.
            if (_canWriteResponseBody)
            {
                if (_autoChunk)
                {
                    if (data.Length == 0)
                    {
                        return await Output.FlushAsync(cancellationToken);
                    }

                    return await Output.WriteChunkAsync(data.Span, cancellationToken);
                }
                else
                {
                    CheckLastWrite();
                    return await Output.WriteDataToPipeAsync(data.Span, cancellationToken: cancellationToken);
                }
            }
            else
            {
                HandleNonBodyResponseWrite();
                return await Output.FlushAsync(cancellationToken);
            }
        }
    }
}
